查看原文
其他

干货 | TESS.IO在大规模集群下的性能优化

Yingnan Zhang eBay技术荟 2022-03-15

供稿 | eBay TESS.IO Team

本文3844字,预计阅读时间10分钟

更多干货请关注“eBay技术荟”公众号



导读

Tess.IO是eBay的应用程序集群管理器,旨在为eBay大规模开发和运行应用程序提供世界一流的保障,使程序开发人员的工作更加高效、安全和灵活。随着越来越多的应用程序部署在Tess.IO集群上,集群的可扩展性及扩展后的良好性能变得越来越重要。本篇介绍了Tess.IO团队如何对大规模集群的性能进行卓有成效的优化。

开源容器集群管理器Kubernetes(通常称为“k8s”)是Tess.IO的上游平台,理论上,Kubernetes声称可以支持集群中5000个节点的运行,但在实际操作中却很难达到。原因在于:


其一,为了使Tess.IO集群更好地运行,我们在每个节点上部署了附加组件,例如,network agent(专为pods配置网络的网络代理),beats(节点监控信息的心跳记录),和node-problem-detector(节点问题检测器)等,这些附加组件都需要与集群的控制平台进行交互。


其二,云本地的客户端(cloud-nativecustomer pods)会使用CustomResourceDefinition,这为集群添加了更多负载量。

 

本文用kubemark进行模拟实验,介绍了Tess.IO是如何在以上因素的限制下,依然能够实现流畅运行5000个节点的大规模集群的目标。



Tess.IO集群架构


在讨论集群的可扩展性之前,首先要明确Tess集群的部署结构。(如下图)

 

 

为了实现99.99%的可靠性目标,我们在Tess.IO集群中部署了五个主节点(Master nodes)来运行Kubernetes核心服务(apiserver,控制器管理器,调度程序,etcd和etcdsidecar等)。


除核心服务外,每个节点还有Tess插件,用于公开指标,设置网络或收集日志。以上所有插件都会监控集群控制平台的资源,这给kubernetes控制平台带来额外的负担。 


测试用例


在明确了集群架构以后,我们通过Kubemark进行集群的扩展性测试。


Step1

首先,明确Tess集群中当前部署的etcd/ apiserver能否承受来自控制平台和系统守护进程(daemonsets)组件的压力。因为来自守护进程和kubelet的api负载会随着节点数的增加而增加,而且这是集群控制层面自身的压力,因此这是测试的基本操作。

 

Step2

然后,明确Tess集群在内置基础api负载下响应客户端变更的能力。例如,同时创建/删除1000个pods,查看错误率,QPS和延迟的百分率。


Step3 

此外,还应明确Tess集群能够承受多少来自客户端pods的压力。例如,watcher的上限是多少,apiserver可以承担多少LIST请求。


模拟测试环境


除了测试用例本身,我们还需要模拟测试环境,以接近真实运行环境。

为了模拟5000个节点,我们在集群B中启动了5000个kubemark pod并将其注册到集群A的apiserver。(如下图)


                           

这样执行有四个优势:

  • 可以在eBay数据中心节省5000个虚拟机

  • 使用kubernetes集群B(私有IP)可以轻松扩展5000个节点

  • 可以保存目标集群的公共IP(公共IP在eBay数据中心全局可路由)

  • 可以隔离kubemark节点和目标集群A之间的影响

 

此外,节点上其他组件的运行也需模拟,但只是抽象与apiserver的交互逻辑,并将其构建为容器添加到kubemarkpod中。由此,一个kubemark pod包含了一个模拟节点,以及该节点上的守护进程集群。

 


测试问题陈述


在运行上述测试时,出现了以下问题:


1

无法从故障中恢复

(当有5k个节点,150k个pods时)


  • 每个主节点上ETCD都会占用大量记忆存储(大约100GB)


  • ETCD频繁进行leader选举

in the etcd log:

2018-07-02 17:17:43.986312 W | etcdserver: apply entries tooktoo long [52.361859051s for 1 entries]

2018-07-02 17:17:43.986341 W | etcdserver: avoid queries withlarge range/delete range!


  • apiservers容器每隔几分钟持续重启


2

调度程序在大型集群中很慢


  • 当集群中只有5k个节点,没有pod的时候,调度1k个pod大约需要20分钟,平均每个pod需要1~2秒。然而,当有5k个节点和30k个pod时,每个pod的平均调度时间要长达1分钟。

3

大量查询会破坏集群


  • 在Tess.IO集群中,pod是代表应用程序实体的重要资源,因此集群可扩展性的指标不仅要看节点数,还要看pod数。

  • pods信息对集群控制平台的组件和客户端应用程序十分重要,因为两者都需要进行pods查询。

  • 随着应用程序数量的增加,容器数量将变得非常大,对pods资源的LIST请求将成为LargeRange请求,大量的并发LIST请求可能完全占用kube-apiserver的缓冲区窗口,并对核心探测器产生影响。


4

 Etcd周期性改变leader


  •  Tess.IO集群每半小时都会获取一次etcd快照,而etcd会在快照期间更改leader。



解决方案


一般来说,对于大规模集群,首先要将(max mutating in flight)参数请求设置为1000。对于5k个节点,如果只考虑kubelet的PATCH请求(节点每10秒汇报一次心跳),由于部署了5个master node,那么平均每个apiserver的每秒写操作数是100。

 

此外,NPD(Node-Problem-Detector)和其他组件也会同时PATCH节点,如果用于patch node的tcp长链接到5个apiserver实例的分布不均匀,或者某些apiserver宕掉,亦或是patch操作受到长的READ事务的影响,那么很容易触碰到默认200的inflight-limit,请求就会被拒绝(错误码429)。这样一来,请求就会被频繁重试,给apiserver带来更大的压力。



下面针对测试出现的四大问题各个击破


问题1. 无法从故障中恢复

在每个节点上,都会有一些守护进程用于配置网络、收集指标/日志或报告硬件信息。因此,除了kubelet以外,这些守护进程也会监视节点上的所有pods。

当大规模集群无法从故障中恢复,主要有三种可能:


No.1 守护进程覆盖ListOption

根本原因:

在默认的LIST/WATCH机制中,第一个请求是LIST调用,并且只会从apiserver  cache中获取对象列表并不到达etcd。在注册自定义ListFunc和WatchFunc时,假如守护进程覆盖了ListOption,那么这些请求仍将穿透apiserver到达etcd,这样一来,如果重新启动apiserver或重新部署守护进程,所有pods的LIST请求都将命中etcd,这对于kube-apiserver将是一场灾难。


改进措施:

确保所有守护进程在自定义ListFunction的时候不会覆盖ListOption,这样就从apiserver缓存而不是通过etcd获取资源列表。

 

No.2 podwatchcache未准备好

根本原因:

每个节点上都有5个守护进程组件对pods发起LIST/WATCH请求,这些组件通过fieldSelector向pods发起问询(第一个LIST请求是通过resourceVersion=0和nodename=<nodeName>发送的)。

由于pods数量非常大,因此podwatchcache准备就绪需要几秒钟,如果podwatchcache没有准备好,那么apiserver就会直接将这些LIST请求发送给etcd。此外,当apiserver重启时,所有podwatcher还会并行重新发送LIST请求,且依旧发送给etcd,这对于etcd来说是一个巨大的压力。


改进措施:

在监视到缓存未准备好时及时返回错误。(如下图) 


No.3 apiserver过滤压力大

问题陈述:

缓存准备就绪后,所有LIST请求将再次并行发送给apiserver。虽然etcd集群的状态正常,但是压力转移到了apiserver。通过pprof可以看到,apiserver正忙于过滤那些pods数量超过150k的节点上的pods。(如下图)

 

根本原因:

apiserver过滤压力大,是因为GetAttrs从目标处理对象中获取字段和标签并将其设置为golang地图,而设置地图是一项耗时的操作。在1.10之前的版本中,这项工作通常在filterloop中被调用,如果有150k个pods,那么在每个使用fieldSelector/ labelSelector的LIST请求中,它将被调用15万次。


改进措施: 

将GetAttrs移动到统一的位置,当把处理对象从etcd移到apiserver缓存时,GetAttrs就会获取到该对象的属性,并将属性和对象一起存储在缓存中。这样一来,apiserver和etcd就可以承受突发的压力了,比如服务器重启或用户重新同步LIST/WATCH请求。

 

问题2. 调度程序在大型集群中速度很慢

根本原因:

首先明确Pod在以下两种状态会拖慢程序的调度效率(即使只有一个pod也会如此)。

  • Pod处于Terminating状态

  • Pod所在的node丢失

在实际产品环境中,上述问题十分常见,因此改善这两种情况对调度大型集群非常重要。

 

改进措施:

  • 改进FilteredList()函数,因为该函数是慢速路径。

  • 将Mutex更改为RWMutex

  • 删除多次调用函数所产生的字符串连接

  • 避免较大的序列增长

  • 即使存在pod所在的节点信息被删除,也要创建元数据,集群调度就会在上述情况下绕过慢路径。

 

改进原理:

引入元数据可以更好地用affinity/anti-affinity规则调度pods。

通过使用satisfiesPodsAffinityAntiAffinity()函数查找集群中与affinity/anti-affinity规则所匹配的pods,并存储为元数据,这样FilteredList()函数运行时只需要检查每个节点上可匹配规则的pods即可,从而大大缩短运行延迟。


  • satisfiesPodsAffinityAntiAffinity()函数写为:

satisfiesPodsAffinityAntiAffinity()- Checks if scheduling the pod onto this node would break any rules of this pod

 

改进后再测试:

基于5000个节点,170k个现存pods(包括节点已被删除的处于Terminating状态的pods),我们创建1000个使用了anti-affinity规则的pod,然后测试其调度延迟。(如下图)


测试结果:

平均每秒调度15.2个pods(未改进之前每个pod需要7分钟)

 

问题3. 大量的查询会破坏集群

根本原因:

Pods和节点是Tess.IO集群中最大的两种资源,但在etcd中存在全局锁,pods和节点这两种资源会相互竞争全局锁,从而导致生产力降低。

此外,当pods的LIST请求也在并行(命中etcd)时,节点的PATCH将受到影响,如果节点PATCH长时间不能成功,节点将变为NotReady状态,这对于集群是很危险的。

 

改进措施a:将pods信息存储在单独的etcd里

考虑到节点的PATCH请求是集群运行中最频繁的请求,因此将Pods资源分离出专用的etcd,一方面可以避免影响节点的PATCH,另一方面也有助于扩展集群规模。

 

改进措施b:将Rate-Limit增至大型LIST请求

在集群测试中,如果同时创建/删除少于1000个pods,那么apiserver或etcd就可以承受这种压力。然而,如果所有核心组件都使用SharedInformer/ ListWatch机制,控制平台甚至可以同时重启5个apiserver。此时,所有客户端发送的请求都会命中apiserver缓存而不是旁路apiserver缓存。

因此,将Rate-Limit增至大型LIST请求,通过旁路apiserver缓存,这样可以避免给etcd过多负载。



如上图所示,我们基于吞吐量设定Rate-Limit。

具体而言,记录访问比较频繁且大量的LIST请求的运行模式和运行成本(这些信息可以通过etcd接收,并从apiserver发送至客户端),然后根据已有记录来预测未来LIST请求的运行成本并执行Rate-Limit。

 

问题4. Etcd持续改变leader

根本原因:

Etcd将raftproposal记录至WAL(write-aheadlogging)日志中(以维护数据一致性)。从上方贴出的etcd日志中可以看出,WAL同步的持续时间大于1秒,IO被占用,那么etcd集群的leader可能或错过心跳汇报,导致请求超时和leader丢失,那么其他followers就会开始发起新的leader选举。



如上图所示,所有“WAL同步时间较长”的时间点都与“etcd备份”的时间点完全匹配。

 

改进措施:将etcd数据备份到独立磁盘

用iostat测试etcd数据盘(下图中的sdb)的读写速率以及io使用率。在大多数情况下,每秒写入字节数为20MB/ s~60MB / s,但在每半小时快照状态中,每秒写入字节数约为500MB/ s,并且IO利用率高达100%。


根据上图,我们猜测这是由于刷新etcd备份文件引起的。由于目前etcd备份与etcd数据共享同一磁盘,测试时快照大小为4GB,etcd备份占用了所有磁盘IO,并影响ETCD同步WAL日志的常规操作。

Tess.IO集群使用的内核版本是3.10.0-862.3.2.el7.x86_64,默认IO调度算法是deadline,而不是cfq(为每个队列分配访问磁盘的时间片),基于deadline的IO调度算法会加剧刷新备份占用磁盘IO的情况。

 

为了解决这一问题,我们获取快照并将其写入另一块与etcd数据盘不同的磁盘。这样一来, WAL同步时长的峰值消失了,也没有发生频繁进行leader选举的问题。

 

【 总 结

Kubernetes声称在一个集群中可以支持5000个节点,但在实际操作中,集群中的大量现有资源(例如pods)、不同的部署结构和不同的API负载模式都会限制其可承载量的真实大小。


如今,经过调整和修复,eBay Tess.IO已经成功实现了大规模集群的性能优化,为程序开发人员提供更好的运行体验。







 作者简介 


Yingnan Zhang,中国科学技术大学毕业,2016年加入eBay,现任eBay资深云计算工程师,负责kubernetes平台核心组件的高可用方案的设计和维护,拥有多年高性能运算以及云计算经验。




您可能还感兴趣:

干货 | eBay Hadoop/Spark自助分析系统实践

干货 | Unicorn—近实时系统的自动化修复工具

干货 | 实践Hadoop MapReduce 任务的性能翻倍之路

干货 | 深度解析NetScaler的七层规则处理优先级

干货 | HTTPS:除了保障安全,eBay还能做到更多!




快!关注这个公众号,一起涨姿势~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存